Sistemas y Señales Biomédicos

Ingeniería Biomédica

Ph.D. Pablo Eduardo Caicedo Rodríguez

2026-01-14

Sistemas y Señales Biomedicos - SYSB

Introduction to data adquisition

  • There are two main roles in data: capture the information and encode the data in a form tha machine can process.
  • Data adquisition has three stages:
    • Transduction
    • Signal conditioning
    • Analog-to-digital conversion

Introduction to data adquisition - Transduction

  • Transduction is the conversion from one form of energy to another.
  • The only energy suitable for computer processing is the electrical
  • Therefore signals need to be converted to analog voltages whose waveforms are ideally the same as those of the original signals.
  • Exist two components a captured signal: one component carries the information (signal), the other one is a probabilistic distorsion of the information(noise)

Introduction to data adquisition - Noise

Definition

Noise refers to any unwanted or random variations in a signal that interfere with the desired information. It is an unpredictable disturbance that can distort or obscure the actual data, making it harder to interpret or analyze.

Types of noise

  • Thermal Noise (Random Noise)
  • Electromagnetic Interference (EMI)
  • Motion Artifacts
  • Physiological Noise
  • Quantization Noise

Introduction to data adquisition - Noise

Modelling the noise

  • Additive White Gaussian Noise (AWGN): Modeled as a random process with a normal distribution.
  • Band-limited Noise: Affects only specific frequency ranges and can be removed with filters.
  • Additive Noise: Adds directly to the original signal.
  • Multiplicative Noise: Multiplies the original signal.

Introduction to data adquisition - Noise

import numpy as np
import matplotlib.pyplot as plt

# Parámetros de la señal
duration = 2  # Duración en segundos
fs = 1000  # Frecuencia de muestreo en Hz
t = np.linspace(0, duration, duration * fs, endpoint=False)  # Vector de tiempo

# Señal senoidal de 10 Hz
freq = 10
sine_wave = np.sin(2 * np.pi * freq * t)

# Señal de ruido aleatorio con distribución normal
noise_normal = np.random.normal(0, 1, len(t))

# Señal con ruido aleatorio de 2 a 5 Hz
low_freq_noise = np.sin(2 * np.pi * np.random.uniform(2, 5) * t)
signal_with_low_freq_noise = sine_wave + low_freq_noise

# Señal con ruido aleatorio uniforme sumado
uniform_noise = np.random.uniform(-0.5, 0.5, len(t))
signal_with_uniform_noise = sine_wave + uniform_noise

# Señal con ruido aleatorio uniforme multiplicado
multiplicative_noise = np.random.uniform(0.5, 1.5, len(t))
signal_with_mult_noise = sine_wave * multiplicative_noise

# Graficamos las señales
fig, axes = plt.subplots(5, 1, figsize=(10, 10), sharex=True)

axes[0].plot(t, sine_wave, label="Sine wave (10 Hz)")
axes[0].set_title("Sine Wave (10 Hz)")
axes[0].legend()

axes[1].plot(
    t, noise_normal, label="Random Noise (Normal Distribution)", color="orange"
)
axes[1].set_title("Random Noise (Normal Distribution)")
axes[1].legend()

axes[2].plot(
    t, signal_with_low_freq_noise, label="Sine + Low Freq Noise (2-5 Hz)", color="green"
)
axes[2].set_title("Sine + Low Freq Noise (2-5 Hz)")
axes[2].legend()

axes[3].plot(t, signal_with_uniform_noise, label="Sine + Uniform Noise", color="red")
axes[3].set_title("Sine + Uniform Noise")
axes[3].legend()

axes[4].plot(t, signal_with_mult_noise, label="Sine * Uniform Noise", color="purple")
axes[4].set_title("Sine * Uniform Noise")
axes[4].legend()

plt.xlabel("Time [s]")
plt.tight_layout()
plt.show()

Introduction to data adquisition - ASP

Definition

Analog signal processing (ASP) refers to the manipulation of continuous-time signals after they have been acquired from a transducer but before digital conversion. This type of processing is performed using electronic circuits that modify the signal in the analog domain to enhance its quality, extract useful information, or prepare it for further processing.

Common tasks

  • Amplification: Increases the signal strength to match the required voltage levels. Example: ECG signals are weak (~1 mV) and need to be amplified before analysis.
  • Filtering: Removes unwanted frequency components such as noise or interference.
  • Modulation/Demodulation: Used for communication systems where signals are modulated onto a higher-frequency carrier wave. Example: Biomedical telemetry systems use amplitude modulation (AM) or frequency modulation (FM) to transmit patient data wirelessly.
  • Differentiation & Integration: Differentiation: Highlights rapid changes in the signal. Example: Used in QRS detection for ECG signal analysis. Integration: Smooths out signals and accumulates values over time. Example: Used in electromyography (EMG) processing to estimate muscle activation.
  • Signal Conditioning: Includes impedance matching, offset correction, and dynamic range adjustments. Example: Removing DC offsets in biosignals before digitization.

Introduction to data adquisition - analog-to-digital convertion

Definition

An analog-to-digital converter (ADC) is a device that converts a continuous-time signal, obtained through a transducer, into a digital signal that can be processed by a computer. This process consists of two fundamental operations, which occur simultaneously in practical implementations: sampling and quantization.

Operations

  • Sampling involves converting the continuous-time analog signal into a discrete-time signal, where the amplitude remains unrestricted.
  • Quantization then maps this continuous-amplitude signal to a finite set of discrete values, making it fully digital.

Quantization in DSP: Purpose and Context

  • Quantization maps continuous amplitudes to a finite set of levels to enable digital representation.
  • In acquisition chains: anti-alias filter → sampling → ADC quantization.
  • Quantization introduces an error that behaves like noise under standard assumptions.
  • Biomedical relevance: ECG, EEG, EMG, and PPG require appropriate bit depth, gain, and dynamic range to preserve diagnostically relevant features.

Uniform Quantizer: Definitions

  • Input range: \(\left[V\_{\min},,V\_{\max}\right]\), bit depth \(b\), levels \(L=2^b\), step size (LSB)

    \[ \Delta=\frac{V_{\max}-V_{\min}}{L}. \]

  • Mid-tread (round-to-nearest): \(Q(x)=\Delta,\mathrm{round}!\big(x/\Delta\big)\).

  • Mid-rise (truncate + half-step): \(Q(x)=\Delta\big(\lfloor x/\Delta\rfloor+\tfrac12\big)\).

  • Overload/clipping outside \(\left[V\_{\min},V\_{\max}\right]\): \(\tilde{x}=V\_{\max}\) or \(V\_{\min}\).

Decision Thresholds and Codebook

  • Decision thresholds at \(k\Delta\); reconstruction levels at:

    • \(k\Delta\) (mid-tread), or
    • \((k+\tfrac12)\Delta\) (mid-rise).
  • Practical note: choose mid-tread for rounding semantics; mid-rise for deterministic staircase without zero level.

Quantization Error: Model and Power

  • Error \(e=x-Q(x)\). Under high-resolution assumptions (no clipping, sufficiently dense input):

    • \(e=x-Q(x)\sim\mathcal{U}\left[-\tfrac{\Delta}{2},\tfrac{\Delta}{2}\right]\), \(\mathbb{E}\left[e\right]=0\), \(\mathrm{Var}(e)=\Delta^2/12\).
  • For a full-scale sinusoid:

    \[ \mathrm{SNR}_{\mathrm{dB}}\approx 6.02\,b + 1.76. \]

  • With RMS usage fraction \(\rho\) of full scale (FS):

    \[ \mathrm{SNR}_{\mathrm{dB}}\approx 6.02\,b + 1.76 + 20\log_{10}(\rho). \]

Effective Number of Bits (ENOB)

  • From a measured in-band SNR (RMS, same bandwidth):

    \[ \mathrm{ENOB}\approx\frac{\mathrm{SNR}_{\mathrm{dB}}-1.76}{6.02}. \]

  • Use ENOB to compare real converters (including clock jitter, distortion) against ideal \(b\).

Input Range, Analog Gain, and Clipping

  • Analog gain \(G\) maps input to ADC: \(x\_{\text{ADC}}=G,x\_{\text{in}}\).

  • Input-referred LSB: \(\Delta\_{\text{in}}=\Delta/G\).

  • Design goals:

    1. Avoid overload for rare peaks; 2) Use a large fraction of FS (e.g., \(50\)\(80%\)) to improve SNR.

Biomedical Example: ECG Acquisition

  • Suppose electrode-level ECG peaks \(\approx \pm 5,\mathrm{mV}\). Choose \(G=200\) so \(\pm 5,\mathrm{mV}\mapsto \pm 1,\mathrm{V}\) at ADC (\(V\_{\min,\max}=\pm 1,\mathrm{V}\)).

  • With \(b=12\):

    • \(\Delta=\dfrac{2,\mathrm{V}}{2^{12}}\approx 0.488,\mathrm{mV}\) (ADC domain).
    • \(\Delta\_{\text{in}}=\Delta/G\approx 2.44,\mu\mathrm{V}\).
    • Input-referred noise RMS \(\sigma\_q=\Delta\_{\text{in}}/\sqrt{12}\approx 0.704,\mu\mathrm{V}\_{\mathrm{RMS}}\).
  • If \(\rho\approx 0.5\), then \(\mathrm{SNR}\approx 6.02\cdot 12 + 1.76 - 6 \approx 68,\mathrm{dB}\) → typically adequate for diagnostic ECG.

Non-Uniform Quantization and Companding (Brief)

  • For highly non-uniform amplitude distributions, companding allocates effective resolution to small amplitudes.

  • \(\mu\)-law (telephony):

    \[ y=\mathrm{sgn}(x)\,\frac{\ln\big(1+\mu |x|/X_{\max}\big)}{\ln(1+\mu)},\quad \mu\approx 255. \]

  • In biomedicine, primary acquisition usually remains linear; companding is more relevant to low-bit-rate telemetry or storage.

Dither: When and Why

  • Add small white noise (RMS \(\approx \Delta/2\)) before quantization to decorrelate error, eliminate bias/patterning at low levels, and linearize averages.
  • Slight SNR penalty but improved fidelity for low-level features (e.g., EEG baselines).

Practical Design Checklist

  • Choose \(b\) to exceed clinical SNR requirements by \(10\)\(20,\mathrm{dB}\).
  • Set \(G\) so typical peaks use \(50\)\(80%\) FS; verify worst-case spikes do not clip.
  • Full noise budget: electrode + amplifier + ADC quantization + clock jitter (for high \(f\)).
  • Validate with calibrated sources and report ENOB over the intended bandwidth.

Python Demo: ECG Quantization at Multiple Bit Depths

# Synthetic ECG, quantization at 8/10/12 bits, SNR and plots
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

fs = 360.0
T = 5.0
t = np.arange(0, T, 1/fs)

def g(t, mu, sigma, A):
    return A*np.exp(-0.5*((t-mu)/sigma)**2)

def ecg_template(t):
    P = g(t, 0.20, 0.045,  0.10)
    Q = g(t, 0.36, 0.010, -0.25)
    R = g(t, 0.40, 0.012,  1.00)
    S = g(t, 0.44, 0.016, -0.35)
    Tn= g(t, 0.70, 0.080,  0.30)
    return P + Q + R + S + Tn

hr = 60.0
RR = 60.0/hr
ecg_mV = np.zeros_like(t)
for k in range(int(np.ceil(T/RR))):
    ecg_mV += ecg_template(t - k*RR)

wander = 0.05*np.sin(2*np.pi*0.3*t)
noise  = 0.02*np.random.randn(len(t))
ecg_mV = ecg_mV + wander + noise

G = 200.0
Vfs = 1.0
Vmin, Vmax = -Vfs, Vfs
x_adc = (ecg_mV/1000.0)*G  # mV -> V and gain

def quantize_uniform(x, bits, Vmin, Vmax, mid_tread=True):
    x_clip = np.clip(x, Vmin, Vmax)
    L = 2**bits
    Delta = (Vmax - Vmin)/L
    if mid_tread:
        y = Delta*np.round(x_clip/Delta)
    else:
        y = Delta*(np.floor(x_clip/Delta) + 0.5)
    y = np.clip(y, Vmin, Vmax)
    return y, Delta

def snr_db(x, y):
    e = x - y
    x_ac = x - np.mean(x)
    e_ac = e - np.mean(e)
    Px = np.mean(x_ac**2)
    Pe = np.mean(e_ac**2)
    return 10*np.log10(Px/Pe), e

bits_list = [8, 10, 12]
results = {}
for b in bits_list:
    y_adc, Delta = quantize_uniform(x_adc, b, Vmin, Vmax, mid_tread=True)
    snr, e = snr_db(x_adc, y_adc)
    results[b] = dict(y_adc=y_adc, Delta=Delta, snr_db=snr, err=e)

print("Summary (ADC domain):")
Summary (ADC domain):
for b in bits_list:
    print(f"{b:2d}-bit -> LSB Δ = {results[b]['Delta']*1e3:.3f} mV, Measured SNR ≈ {results[b]['snr_db']:5.1f} dB")
 8-bit -> LSB Δ = 7.812 mV, Measured SNR ≈  24.1 dB
10-bit -> LSB Δ = 1.953 mV, Measured SNR ≈  36.0 dB
12-bit -> LSB Δ = 0.488 mV, Measured SNR ≈  48.1 dB
Delta_in = results[12]["Delta"]/G
sigma_q_in = Delta_in/np.sqrt(12)
print(f"\nInput-referred (12-bit): Δ_in = {Delta_in*1e6:.3f} µV, σ_q ≈ {sigma_q_in*1e6:.3f} µV RMS")

Input-referred (12-bit): Δ_in = 2.441 µV, σ_q ≈ 0.705 µV RMS
# Plot 1: original vs quantized (10-bit)
b_plot = 10
idx = (t >= 1.5) & (t <= 2.7)
plt.figure()
plt.title(f"ECG (ADC input) vs. {b_plot}-bit quantized")
plt.plot(t[idx], x_adc[idx], label="Original (ADC input)")
plt.plot(t[idx], results[b_plot]["y_adc"][idx], label=f"{b_plot}-bit")
plt.xlabel("Time [s]")
plt.ylabel("Amplitude [V]")
plt.legend()
plt.grid(True)
plt.show()

# Plot 2: quantization error histogram (8-bit)
b_err = 8
plt.figure()
plt.title(f"Quantization error histogram ({b_err}-bit)")
plt.hist(results[b_err]["err"], bins=80, density=True)
plt.xlabel("Error [V]")
plt.ylabel("PDF estimate")
plt.grid(True)
plt.show()

Analog to digital convertion

To explain the analog-to-digital conversion process, we will assume that the input signal is a cosine wave with frequency \(F\), angular frequency \(\Omega\) and amplitude \(a\).

\[x\left(t\right) = a \cos\left(\Omega t + \phi\right) = a \cos\left(2\pi F t + \phi\right)\]

Obtaining

\[x\left[n\right] = a \cos\left(\omega n + \phi\right) = a \cos\left(2\pi f n + \phi\right)\]

Analog to digital convertion

Analog to digital convertion

What?

Mathematically, the sampling process is:

\[x[n] = x(nT_s), \quad -\infty < n < \infty\]

Replacing in previous equations, we have the expression:

\[x[n] = x(nT_s) = a \cos\left( 2\pi F n T_s + \phi \right) = a \cos\left( 2\pi n \frac{F}{F_s} + \phi \right) \]

Where:

\[\omega = \Omega T_s, \quad f = \frac{F}{F_s}\]

Sample and quantization of an ECG signal

  • Generate a synthetic ECG-like signal.
  • Sample it at different rates.
  • Apply quantization with different bit depths.

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import chirp

# Generate a synthetic ECG-like signal (chirp function as approximation)
fs_original = 10000  # High sampling rate (Hz) - "continuous" signal
t = np.linspace(0, 1, fs_original, endpoint=False)  # 1-second signal
signal = np.sin(2 * np.pi * 1.7 * (t**2))  # Simulated chirp (similar to ECG waves)

# Downsample (Sampling Process)
fs_sampled = 200  # Sampling frequency in Hz (e.g., ECG sampled at 200 Hz)
t_sampled = np.arange(0, 1, 1/fs_sampled)
signal_sampled = np.sin(2 * np.pi * 1.7 * (t_sampled**2))

# Quantization (8-bit and 4-bit)
def quantize(signal, bits):
    levels = 2**bits
    min_val, max_val = signal.min(), signal.max()
    step = (max_val - min_val) / levels
    quantized_signal = np.round((signal - min_val) / step) * step + min_val
    return quantized_signal

signal_quantized_8bit = quantize(signal_sampled, 8)
signal_quantized_4bit = quantize(signal_sampled, 4)

# Plot Results
plt.figure(figsize=(12, 6))

# Original vs Sampled Signal
plt.subplot(2, 1, 1)
plt.plot(t, signal, 'k', alpha=0.3, label='Original Signal (High Resolution)')
plt.plot(t_sampled, signal_sampled, 'ro-', label=f'Sampled Signal ({fs_sampled} Hz)')
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.legend()
plt.title("Sampling Process")

# Quantized Signals
plt.subplot(2, 1, 2)
plt.plot(t_sampled, signal_sampled, 'bo-', alpha=0.5, label="Original Sampled")
plt.plot(t_sampled, signal_quantized_8bit, 'go-', label="Quantized 8-bit")
plt.plot(t_sampled, signal_quantized_4bit, 'ro-', label="Quantized 4-bit")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.legend()
plt.title("Quantization Effect")

plt.tight_layout()
plt.show()